﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using Tesla.Graphics;
using Tesla.Math;

namespace Tesla.Util {
    /// <summary>
    /// Memory utilites, used primarily for moving large amounts of byte data quickly.
    /// </summary>
    public static class MemoryHelper {
        private static Dictionary<Object, GCHandle> _pinnedObjects = new Dictionary<Object, GCHandle>();

        /// <summary>
        /// Calculate the size of the mip level. Use this to compute the actual size of a sub-resource
        /// that uses a compression format.
        /// </summary>
        /// <param name="width">Width of the first mip level</param>
        /// <param name="height">Height of the first mip level</param>
        /// <param name="depth">Depth of the first mip level</param>
        /// <param name="mipLevel">Mip level to access</param>
        /// <param name="format">Format of the resource</param>
        /// <returns>Total size of the sub-resource in bytes</returns>
        public static int CalculateMipSize(int width, int height, int depth, int mipLevel, SurfaceFormat format) {
            int formatSize = 0;
            switch(format) {
                case SurfaceFormat.Vector4:
                    formatSize = 16;
                    break;
                case SurfaceFormat.Vector3:
                    formatSize = 12;
                    break;
                case SurfaceFormat.Vector2:
                    formatSize = 8;
                    break;
                case SurfaceFormat.Single:
                case SurfaceFormat.Color:
                case SurfaceFormat.RGBA1010102:
                case SurfaceFormat.RG32:
                    formatSize = 4;
                    break;
                case SurfaceFormat.BGR565:
                case SurfaceFormat.BGRA5551:
                    formatSize = 2;
                    break;
                case SurfaceFormat.DXT1:
                case SurfaceFormat.DXT3:
                case SurfaceFormat.DXT5:
                case SurfaceFormat.Alpha8:
                    formatSize = 1;
                    break;
            }
            int mipWidth = (int) MathHelper.Max(1, width >> mipLevel);
            int mipHeight = (int) MathHelper.Max(1, height >> mipLevel);
            int mipDepth = (int) MathHelper.Max(1, depth >> mipLevel);
            //Get actual size for compressed data
            if(format == SurfaceFormat.DXT1 || format == SurfaceFormat.DXT3 || format == SurfaceFormat.DXT5) {
                mipWidth = (mipWidth + 3) >> 2;
                mipHeight = (mipHeight + 3) >> 2;
                mipDepth = (mipDepth + 3) >> 2;
                formatSize = (format == SurfaceFormat.DXT1) ? 8 : 16;
            }
            return formatSize * mipWidth * mipHeight * mipDepth;
        }

        /// <summary>
        /// Same as Marshal.SizeOf(Type)
        /// </summary>
        /// <param name="type">Type</param>
        /// <returns>Size in bytes</returns>
        public static int SizeOf(Type type) {
            return Marshal.SizeOf(type);
        }

        /// <summary>
        /// Same as Marshal.SizeOf(typeof(T))
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns>Size of the type in bytes</returns>
        public static int SizeOf<T>() {
            return Marshal.SizeOf(typeof(T));
        }

        /// <summary>
        /// Same as Marshal.SizeOf(Object)
        /// </summary>
        /// <param name="obj">Object</param>
        /// <returns>Size in bytes</returns>
        public static int SizeOf(Object obj) {
            return Marshal.SizeOf(obj);
        }

        /// <summary>
        /// Checks an array's bounds for copy operations.
        /// </summary>
        /// <param name="array">Array to check</param>
        /// <param name="offset">Offset from the start of the array</param>
        /// <param name="count">Number of elements that will be copied</param>
        /// <exception cref="System.ArgumentNullException">Throws if the array is null</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Throws an exception if the count is less than zero, 
        /// if the index is out of range, or if the sum of count and index are greater than the array length</exception>
        public static void CheckArrayBounds(Array array, int offset, int count) {
            if(array == null) {
                throw new ArgumentNullException("Array is null");
            }
            CheckBounds(0, array.Length, offset, count);
        }

        /// <summary>
        /// Checks bounds for reading/writing operations.
        /// </summary>
        /// <param name="lowerBound">Lower bound for the index (typically zero)</param>
        /// <param name="length">Length of the data</param>
        /// <param name="offset">Offset index</param>
        /// <param name="count">Number of elements</param>
        /// <exception cref="System.ArgumentOutOfRangeException">Throws an exception if the count is less than zero, 
        /// if the index is out of range, or if the sum of count and index are greater than the array length</exception>
        public static void CheckBounds(int lowerBound, int length, int offset, int count) {
            if(count < 0) {
                throw new ArgumentOutOfRangeException("Array offset must be greater than or equal to zero.");
            }
            if(offset < lowerBound) {
                throw new ArgumentOutOfRangeException("Offset is out of range.");
            }
            if(offset + count > length) {
                throw new ArgumentOutOfRangeException("Count and offset must be less than or equal to the array length.");
            }
        }

        /// <summary>
        /// Copies byte store from one array to another. Arrays do not have to be the same length,
        /// and array lower bounds are checked, but there may be overflow.
        /// </summary>
        /// <param name="src">Source array</param>
        /// <param name="dest">Destination array</param>
        /// <param name="count">The number of bytes to copy</param>
        /// <exception cref="System.ArgumentNullException">Throws an exception if either array is null</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Throws an exception if the count is less than zero or if the offsets are less than zero</exception>
        public static void Copy(Array src, Array dest, int count) {

            if(src == null || dest == null) {
                throw new ArgumentNullException("Array is null");
            }

            if(count < 0) {
                throw new ArgumentOutOfRangeException("Count must be positive.");
            }

            IntPtr pSrc = MemoryHelper.PinObject(src);
            IntPtr pDest = MemoryHelper.PinObject(dest);

            unsafe {
                byte* bpSrc = (byte*) pSrc.ToPointer();
                byte* bpDest = (byte*) pDest.ToPointer();
                memcpy(bpSrc, bpDest, count);
            }

            MemoryHelper.UnpinObject(src);
            MemoryHelper.UnpinObject(dest);
        }

        /// <summary>
        /// Copies byte store from one array to another. Arrays do not have to be the same length,
        /// and array lower bounds are checked, but there may be overflow.
        /// </summary>
        /// <param name="src">Source array</param>
        /// <param name="srcOffset">Zero-based byte index in the source array</param>
        /// <param name="dest">Destination array</param>
        /// <param name="destOffset">Zero-based byte index in the destination array</param>
        /// <param name="count">The number of bytes to copy</param>
        /// <exception cref="System.ArgumentNullException">Throws an exception if either array is null</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Throws an exception if the count is less than zero or if the offsets are less than zero</exception>
        public static void Copy(Array src, int srcOffset, Array dest, int destOffset, int count) {

            if(src == null || dest == null) {
                throw new ArgumentNullException("Array is null");
            }

            if(srcOffset < 0 || destOffset < 0) {
                throw new ArgumentOutOfRangeException("Offsets must be positive.");
            }

            if(count < 0) {
                throw new ArgumentOutOfRangeException("Count must be positive.");
            }

            IntPtr pSrc = MemoryHelper.PinObject(src);
            IntPtr pDest = MemoryHelper.PinObject(dest);

            unsafe {
                byte* bpSrc = (byte*) pSrc.ToPointer();
                byte* bpDest = (byte*) pDest.ToPointer();
                memcpy(bpSrc + srcOffset, bpDest + destOffset, count);
            }

            MemoryHelper.UnpinObject(src);
            MemoryHelper.UnpinObject(dest);
        }

        /// <summary>
        /// Copies byte store from one IntPtr to another. This does not check array bounds, 
        /// handle with care!
        /// </summary>
        /// <param name="src">IntPtr for source store</param>
        /// <param name="dest">IntPtr for destination</param>
        /// <param name="count">The number of bytes to copy</param>
        /// <exception cref="System.ArgumentNullException">Throws an exception if either pointer is zero</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Throws an exception if the count is less than zero</exception>
        public static void Copy(IntPtr src, IntPtr dest, int count) {

            if(src == IntPtr.Zero || dest == IntPtr.Zero) {
                throw new ArgumentException("Invalid buffer pointers.");
            }

            if(count < 0) {
                throw new ArgumentOutOfRangeException("Count must be positive.");
            }

            unsafe {
                byte* pSrc = (byte*) src.ToPointer();
                byte* pDest = (byte*) dest.ToPointer();
                memcpy(pSrc, pDest, count);
            }
        }

        /// <summary>
        /// Copies byte store from one IntPtr to another. This does not check array bounds, 
        /// handle with care!
        /// </summary>
        /// <param name="src">IntPtr for source store</param>
        /// <param name="srcOffset">Zero-based byte index in the source IntPtr</param>
        /// <param name="dest">IntPtr for destination</param>
        /// <param name="destOffset">Zero-based byte index in the destination IntPtr</param>
        /// <param name="count">The number of bytes to copy</param>
        /// <exception cref="System.ArgumentNullException">Throws an exception if either pointer is zero</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">Throws an exception if the count is less than zero or either startIndex is less than zero</exception>
        public static void Copy(IntPtr src, int srcOffset, IntPtr dest, int destOffset, int count) {

            if(src == IntPtr.Zero || dest == IntPtr.Zero) {
                throw new ArgumentException("Invalid buffer pointers.");
            }

            if(srcOffset < 0 || destOffset < 0) {
                throw new ArgumentOutOfRangeException("Offsets must be positive.");
            }

            if(count < 0) {
                throw new ArgumentOutOfRangeException("Count must be positive.");
            }

            unsafe {
                byte* pSrc = (byte*) src.ToPointer();
                byte* pDest = (byte*) dest.ToPointer();
                memcpy(pSrc + srcOffset, pDest + destOffset, count);
            }
        }

        /// <summary>
        /// Pins an object in memory, which allows us to return a 
        /// pointer to it. This means the garbage collector cannot move
        /// the object around in memory, which may impact performance.
        /// </summary>
        /// <param name="obj">Object to pin</param>
        /// <returns>Object's pointer</returns>
        public static IntPtr PinObject(Object obj) {
            GCHandle handle;

            if(!_pinnedObjects.TryGetValue(obj, out handle)) {
                handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
                _pinnedObjects.Add(obj, handle);
            } 

            return handle.AddrOfPinnedObject();
        }

        /// <summary>
        /// Unpins an object from memory.
        /// </summary>
        /// <param name="obj">Object to unpin</param>
        public static void UnpinObject(Object obj) {
            GCHandle handle;

            if(_pinnedObjects.TryGetValue(obj, out handle)) {
                handle.Free();
                _pinnedObjects.Remove(obj);
            }
        }

        /// <summary>
        /// Copies bytes from one unsafe byte pointer to another, from the MSDN
        /// C# programming guide. This does not check bounds and offsets should be 
        /// added to the pointer before passing them in.
        /// </summary>
        /// <param name="src">Source byte pointer</param>
        /// <param name="dest">Source byte pointer</param>
        /// <param name="count">The number of bytes to copy</param>
        public static unsafe void memcpy(byte* src, byte* dest, int count) {
            //Loop over the byte count and move blocks of 4 bytes (an integer) at a time
            for(int i = 0; i < count / 4; i++) {
                *((int*) dest) = *((int*) src);
                dest += 4;
                src += 4;
            }

            //Grab any bytes that were not moved in blocks of 4
            for(int i = 0; i < count % 4; i++) {
                *dest = *src;
                dest++;
                src++;
            }
        }

        /// <summary>
        /// Enumerates the enumeration.
        /// </summary>
        /// <param name="enumeration">The enumeration.</param>
        /// <returns>The IEnumerable</returns>
        public static IEnumerable<Enum> GetEnumValues(Enum enumeration) {
            List<Enum> enumerations = new List<Enum>();
            foreach(FieldInfo fieldinfo in enumeration.GetType().GetFields(
                BindingFlags.Static | BindingFlags.Public)) {
                    enumerations.Add((Enum) fieldinfo.GetValue(enumeration));
            }
            return enumerations;
        }
    }
}
